UnREST and Its Programming ModelsFawcett Innovations LLC
A conceptual reference for programmersjohn@fawcettinnovations.com
UnREST and Its Programming Models
A conceptual reference for programmers
This document explains how to think about UnREST and the systems built on it. It is an orientation, not a specification — it gives you the mental model so the detailed reference reads as elaboration rather than as a new system to learn.
The order is deliberate: the idea, then the stack that implements it, then the memory it lives in, then the three services that use it.
The FrogNet Living Network · Fawcett Innovations LLC
Contents
What's Inside
The reference, in four moves
01Overview — messages vs shared memory
02The OO model — the client/server stack
03The transient database — where the memory lives
04The three implementations — at increasing depth
05Building a UnRESTful app with AI
It assumes you are comfortable with ordinary web-style programming: HTTP requests and responses, a client talking to a server, a server backed by a database, caches in between. UnREST reuses all of those pieces. What it changes is the default way you think about state — and that one change is what the rest unpacks.
Section One
Overview
The familiar model: messages
Most networked code moves messages. A sender builds a request, sends it; a receiver handles it and sends a response. If you have written REST endpoints, queued jobs, or socket code, this is the world you know. It works, but the hard parts are well known too: messages arrive late, out of order, twice, or not at all — so you end up dealing with retries, deduplication, ordering, acknowledgements, and timeouts. The correctness of the system depends on messages being delivered and handled correctly.
The UnREST model: shared memory
UnREST changes the default. Instead of sending a message to ask a service to do something, you change a piece of shared memory, and a service reacts to the change. Instead of requesting a value from whoever holds it, you read the value, which is simply there.
The default, flipped
Diagram 1. In the familiar model you address a service and manage an exchange. In UnREST you change a named value any node can read; the service reacts to the change. There is no recipient and no event stream — the read is the only event, and it happens because your code asked.
Concretely, state lives as named values any node on the network can read or write:
There is one current value per name. Writing overwrites it in place — no log, no history. If you need to know what changed, compare the new value to the copy you already had.
The value is location-independent. From your code's point of view there is no fetch and no owner — the value is in shared memory and you read it. (Underneath, the network does route to it; the point is that your code does not have to care where it lives.)
A read returns the complete current value, never a change-event or a partial update. You read it whenever you need it, even in a tight loop.
An unchanged read is nearly free. Re-reading a value that hasn't changed collapses to a tiny "nothing changed" marker on the wire. The expensive case in a request/response system — polling something that rarely changes — is the cheap case here.
The academic lineage
This is Linda's tuple-space with two deliberate changes. First, current-value-wins instead of destructively consuming a tuple — so every write is idempotent and every read repeatable. Second, reap instead of block — a read takes the current value if it is there and returns immediately otherwise; nothing ever waits on the network. Read the name literally, too: it is REST turned inside out. In REST you ask a server for a representation of a resource; in UnREST you hold the memory and change it.
Messaging is not gone
One clarification, because the slogan is easy to over-read: UnREST does not abolish messages. Request/response is fully supported, and there is an explicit raw mode — a plain HTTP request in, the full HTTP response back — that you can ask for by name when that is genuinely what you want. What changes is what you reach for first. By default you think in shared memory; you drop to explicit request/response only when you deliberately choose to. As Section 4 shows, one of the three services — discovery — does exactly that, because for its job, asking is the right shape.
What it's made of
Underneath, UnREST is an ordinary client/server stack: an object layer your application programs against, a caching proxy and daemon that move data between nodes, and an Apache + database origin that holds the durable state. The one unusual thing lives in the middle — the proxy and daemon share a model of the data, which is what lets an unchanged read cost almost nothing and makes "messages" behave like "memory." Sections 2 and 3 are that machinery; Section 4 walks the three services built on top of it, which sit at increasing depth.
Section Two
The OO Model — the Client/Server Stack
You program against objects
The programming surface is object-oriented. Your application does not assemble packets or parse wire formats. It works with two kinds of object:
Tuples — the named values in shared memory (covered in Section 3). This is your state.
Handlers — objects that tell the transport everything it needs to know about a kind of data.
There is a single handler interface — "the ONE UnREST handler interface, no special-casing" — and every concrete handler specialises it. A handler has two faces, and it helps to see them separately.
Format handlers — JSON, XML, HTML, text, raw bytes
Teach the transport the structure of a content type. Given an example body, a format handler learns a template, identifies the few fields that actually vary, and can rebuild the full body from the template plus those fields. This is what makes responses compressible: once both ends know the template, an unchanged response is identified by a hash, and a changed one is sent as just its changed fields.
Role handlers — database, media, game
The same kind of object carrying the behaviour that decides which node runs a service and what happens when that changes. They expose score and evaluate to pick a host, and advertise and hostReset to re-assert state when the network shifts (Section 4.1). A handler is not only about formatting.
The three processes (and the fourth tier)
Beneath the object layer are three processes and a database tier. If you picture a caching reverse proxy in front of a web server, you already have the right shape.
The four tiers — a semantic cache pair in front of an origin
Your application
reads / writes OBJECTS — tuples + handlers
↓
PROXY · client side
local semantic cache — REQ_REPEAT = hash only; rebuilds the full value from the template it holds
⇅FrogNet wire (FNW1) · SAME / DIFF · hop-by-hop across the mesh
DAEMON :9009 · server side
re-executes against the backend, compares, replies RESP_SAME (16 B) or RESP_DIFF (changed fields)
⇅plain HTTP at 127.0.0.1
APACHE + api.php + DATABASE
the origin — durable state. The cache pair never becomes the authority.
Diagram 2. The tiers are the classic ones — client, server, database. The proxy is a local semantic cache; the daemon executes against the origin and answers with a difference. Because both ends hold the same handler, only a difference has to cross the wire.
The proxy is the client side — a local semantic cache. Whenever your application reads or writes a value, that is, underneath, an ordinary HTTP request to api.php. The proxy intercepts it, uses the matching handler to template it, and computes a hash of the request. The first time it sees a given request it sends the whole thing; the second time it sends only a repeat — the hash by itself — and when the reply comes back as "unchanged," it reconstructs the full current value from the template it already holds.
The daemon is the server side, listening on a fixed port (:9009). A pool of worker threads executes each request against the backend, templates the result with the same handler, compares it to what it returned last time, and replies one of two ways: SAME (a 16-byte identifier, no body) if nothing changed, or DIFF (the identifier plus only the changed fields) if something did.
One rule, load-bearing
A repeat request always re-executes against the backend. "Repeat" means the question is identical, not that the answer is assumed. The daemon runs the query again and the SAME-or-DIFF decision reflects live data — so a cheap answer is never a stale answer.
Apache, api.php, and the database are the origin — an ordinary LAMP tier holding the durable state, which the daemon reaches at 127.0.0.1. The proxy and daemon are a cache pair in front of the real store; they never become the authority. (One service inverts this: a game host serves its origin from the proxy itself rather than from Apache. The shape is still a semantic cache in front of an origin.)
Here is a single read, end to end, the second time it is asked:
A repeat read, end to end
Diagram 3. The second time a question is asked, the proxy sends only the hash. The daemon still re-runs the query against the origin, then answers in sixteen bytes if nothing changed. The proxy rebuilds the full value locally — a cheap answer that is never a stale one.
Same handler, but not symmetric
The proxy and daemon share the handler, and that shared model is exactly why only a difference has to cross the wire — both ends already hold the structure, so the message shrinks to what changed. But sharing the handler does not mean the two sides do the same thing in both directions. The relationship is asymmetric on purpose. What the proxy says in that shared vocabulary can carry compact or compound meaning the daemon expands and acts on:
a repeat is just the hash of an earlier request — the daemon looks the hash up, recovers the full question, and routes it to the right executor;
a diff is only the changed fields of a request — the daemon reconstructs the whole request from the parts it already has;
a raw request tells the daemon to skip the semantic layer and pass plain HTTP straight through.
The handler gives both sides a common language. The proxy is then free to compose requests in that language that mean more to the daemon than a literal one-to-one HTTP call would — a small token standing for a larger or special operation. Do not assume a request the proxy sends is a mirror of what comes back, or that a one-line request implies a one-line action on the other side.
Why this is "modern" client/server
The tiers are the classic ones, and nothing about Apache or the database is unusual. The difference is the pair in the middle that shares a model of the data. Classic client/server re-sends the entire representation on every request and treats each request as independent. Here, because the proxy and daemon both hold the handler, an unchanged read costs a hash and a changed read costs a diff; and because the server's address is a floating role the mesh resolves, the client neither knows nor cares which physical box answered. Your application is written as though it were reading and writing local objects.
Section Three
The Transient Database — Where the Memory Lives
The shared memory is held in a transient database. "Transient" is the important word: it is durable enough to read reliably, but it is not a system of record. Values carry a timestamp and age out if nobody keeps re-asserting them, and the database itself can move from one node to another. You store coordination and live state here, not your permanent records.
Two roles, and why there are two
The transient is not a single fixed server. It exists as two roles, and understanding the split is the most important thing in this section.
Control plane vs data plane — on the 10/8 network
databasehost_control.frognet
Fixed by a rule — the highest .1 address, always present
Deterministic: every node computes the same answer from the host list, with no negotiation. Holds the inputs to elections — each node's published capability, presence, and the table of services.
databasehost.frognet
Elected for fitness — the most capable box wins, and it floats
Chosen by capability rather than by a fixed rule, so it can be a different node than control. When the network changes, the election re-runs and the role can move. Holds the durable application data — an output.
control reads everyone's capability → runs the selector → elects the data host
Diagram 4. The control host is fixed by a rule (highest .1); the data host is elected and floats. The control host holds the inputs to elections; the data host is one of the outputs. Point "must always resolve the same everywhere" traffic at control, and durable data at the data host.
Tuples: the values themselves
A single value in the transient is a tuple. You address it with a trio — service / variable / scope — and it carries a JSON value you define. Under the hood it maps onto one row of a simple sensor table (the same api.php schema everything else uses):
writer's 10/8 IP→SensorAddress who reported this value
the value→jsonData the JSON blob you define
Diagram 5. There is exactly one row per SensorName, and a write is an upsert — resolve-or-create — so re-writing a value overwrites it in place. No history is kept.
Coordination tuples are marked with an SD: prefix on the name (presence, capability, call signaling, and so on). The prefix lets a dead service's leftover values stay recognizable so they can be reaped. Ordinary observed data — sensor readings, link state — has no prefix and is never a reap candidate, so live coordination state and durable data can share the store without being confused.
Scope: whose value it is, and how it expires
Scope is the part people get wrong, so it is worth spelling out. Four helpers cover the common cases:
host_scope() # "host:<ip>:<pid>" one running instance; goes away when the process exitsnode_scope() # "host:<ip>" one durable row per node, outlives any single processrole_scope("mediahost") # "host:<ip>:<role>" one row per (node, role); two roles never collidesession_scope(call_id) # "session:<id>" scoped to a call / session
host_scope includes the process id, so a long-running service gets a value unique to that instance that disappears when it stops. node_scope drops the pid for a value that must be a single row per node and survive restarts (a periodic advertiser, say). role_scope exists because one node can advertise capability under several roles at once — it might be a candidate for both mediahost and databasehost — and since the key does not encode the service, qualifying the scope with the role keeps those rows from overwriting each other.
Writing and reading
In code, it is two calls:
import frognet_tuples as T
# write / refresh this node's presence
T.put("communicator", "presence", T.host_scope(),
{"name": "Alice", "status": "online"})
# read every node's presence, across all scopesfor r in T.get("communicator", "presence"):
print(r["addr"], "->", r["value"])
put stamps a timestamp so readers can decide whether a value is still fresh. Its own flag controls cleanup: own=True (the default) means this process owns the value and it is removed when the process exits cleanly — right for live state like presence. own=False is a fire-and-forget refresh that must outlive a short-lived writer and age out by staleness instead; a once-a-minute advertiser uses this, because arming cleanup would delete its value moments later and leave the election with nothing to read.
get(service, variable) returns every instance of that variable across scopes; get_all(service) returns every variable for a service. Passing fresh_s=N drops values older than N seconds — the reader's own freshness decision, made the same way by everyone so they agree.
Floating, re-assertion, and robustness
Because the data host can float, writers re-resolve the host and re-assert their values on a heartbeat, and a background reaper deletes whatever stopped being asserted. The reaper, not cleanup-on-exit, is the real guarantee — a crashing node never gets the chance to say goodbye. One more rule keeps reads safe: a single malformed row is dropped and logged, never allowed to abort the read.
Skip the bad row, note it, and return the rest — one corrupt blob must never empty a whole candidate list.
Section Four
The Three Implementations
The three services are the same idea at three depths. Discovery is the shallow end — still moving messages, but so heavily optimized that a stable request is nearly free. The communicator and games are the deep end — they stop thinking in request/response and work in shared memory directly.
Three services, one substrate — at increasing depth
Discovery
probes, asks, waits
Communicator
writes & reads memory
Games
governed memory
MESSAGES — ask and answerSHARED MEMORY — write and read
Diagram 6. The depth is not in the wire — every read and write rides the same proxy/daemon machinery with SAME/DIFF underneath. It is in how each service's own code is written: discovery's code is structured as messages; the communicator's and games' code is structured as shared memory.
4.1 — How a role gets elected
Three of the services run on an elected role — one node the whole network treats as "the one" for a job: the data host, the mediahost, the game engine. Election is not a vote and not a negotiation. It is a deterministic computation each node runs for itself, which is why a split network keeps working on each side and a merged network re-derives the same answer without anyone coordinating. A role is defined by a small handler with two callbacks:
score(candidate)
Given one node's published capability, how fit is it for this role? A number, or "ineligible."
evaluate(candidates)
Given all the candidates and their scores, choose the winner — best score, with the highest IP breaking ties.
The shared election loop — write one handler, register it
gather
read every node's capability from control
→
score
rate each candidate for this role
→
pick
best score · highest IP wins ties
→
publish
a name like databasehost.frognet
Adding a new elected service is just writing score and evaluate — the loop around them is shared.
Diagram 7. Making the criteria a callback is the point: the data host gates on MySQL running and ranks on storage; the mediahost scores on media capability, elected per local network; the game engine treats every eligible node equally and lets the highest IP win.
When the network changes, the same computation runs again and a role can land on a different node. Two more callbacks handle the move: advertise re-publishes a node's capability, and hostReset re-asserts a node's own state after the ground shifts under it. Because an elected role keeps no private state — whatever it has acted on is recorded in the shared object it governs — re-running the same logic over unchanged memory does nothing, so a role can float to a new node without dropping the thread.
4.2 — Discovery: messages, highly optimized
Discovery turns a pile of links into a usable network. It works the oldest way: by asking. Each node probes outward from its own interfaces, asks each address who it is, asks each reachable peer what it knows, and from the answers writes itself a routing table. This is still request/response — and that is deliberate. For a job where reachability has to be re-proven constantly and no node's claim can be trusted on its own, asking is the right shape.
What makes it FrogNet rather than plain HTTP is that the asking is nearly free. Discovery asks the same questions over and over, and almost always the answer is the same as last time — so the responses ride the SAME/DIFF compression from Section 2. A peer whose information hasn't moved since you last asked answers in sixteen bytes.
How a node builds its own routing table
Diagram 8. ".2 proves, .1 carries": you probe a served .2 address to prove the path actually works, and record the .1 the reply carries. Reachability is measured, never asserted — the lowest measured round-trip wins, and an unreachable network is vouched for by a relay. There is no coordinator: every node ends up with its own table.
4.3 — The communicator: session memory
The communicator is a deeper use of the model, and the contrast with discovery is the point: where discovery asks questions and acts on the answers, the communicator never asks anyone anything. It works in shared session state — who is online, who is calling whom, who has joined a call. A participant appears by writing its own presence into the shared region, and drops out simply by ceasing to refresh it. The governing role — the mediahost, elected one per local network — reacts to those writes.
Control plane forms the meeting · data plane carries it
Diagram 9. The clearest example of the control-plane / data-plane split. The rendezvous — presence, call offers, who has accepted — is shared-memory objects in the transient. The heavy payload, the actual audio and video, is not memory: once the session object establishes who is connected, the media streams directly between participants.
4.4 — Games: governed memory
A game is the case that needs a single authority: two players must never see two different boards. So unlike the other two, it has one writer-of-record — the boardgame engine — supplied by the same election that designates every other role, and deliberately kept a different role from the data host so that game authority and data storage need not sit on the same machine.
Players state intent · the engine resolves it · everyone reads the board
Diagram 10. Players never message moves to each other. Each writes its own user object, where a move is stated intent; the engine validates against the rules — authority a player cannot fake by writing memory — and writes the resolved board back. Because the engine records what it applied inside the game object, the engine role can re-elect to a different node and play continues from the last resolved board.
Section Five
Building a UnRESTful App with AI
An AI assistant is a strong collaborator on this kind of system, but it has one specific, predictable failure that will cost you time if you don't watch for it: it quietly re-introduces message-thinking. Request/response, publish/subscribe, send-acknowledge-retry — that is the overwhelming majority of its training, so it is the shape it falls back to whenever it isn't actively held to the model. The symptoms are recognizable:
1
It asks, in effect, "so when the value is sent, who receives it?"
There is no recipient. The read is the only event, and it happens because the reader's code asked.
2
It reaches for "subscribe to the change stream to learn what changed."
There is no stream. You always get the complete current value; if you care what changed, diff it against your own previous copy. An unchanged read is the cheap case, not a wasteful one.
3
It hands the compressor a growing array — a full history that gets longer every tick — and asks it to work out the delta.
The per-tick cost then climbs forever. Send a small bounded tip; keep history resident in memory.
4
It gives a governing role private "what I've already handled" state.
That breaks the ability to float. Record what you have handled inside the governed object, so any node can take the role over and re-running over unchanged memory does nothing.
The model in one sentence
Change memory, not messages; a node writes its own object and an elected role reacts; reads return the complete current value; an unchanged read is free; the carrier conveys structure, not diffs. Give the assistant that up front — then watch for the four tells as the sign it has slipped back into the familiar model.
Two working habits that matter more than anything else
Make it read the actual source before it describes anything. An AI will describe your code confidently from memory, and it will be wrong about your codebase in ways that sound exactly as confident as when it is right. Require it to open the file and read the function before it tells you what the function does.
Make it prove, not assert. For any change, ask for a test on the real system that fails on the old code and passes on the new, and that reproduces the actual failure you saw — not a convenient stand-in. And treat a correction from you as a class of mistake to fix everywhere, not a single line to patch.
Used that way — model first, read before describing, prove before believing — an AI is genuinely helpful on UnREST. Used loosely, it will hand you a clean message-passing system that merely calls its messages "memory."
This conceptual layer is meant to be read first and then set aside. The detailed reference covers the wire format, the exact deterministic election rule, the codec internals, and the database schema — each of which is an instance of the model described here.
The FrogNet Living Network · Fawcett Innovations LLC · john@fawcettinnovations.com